package ch.mzimmermann.sjes;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Event helper implemented in the producer class.
 * registerReceiver and unregisterReceiver should be delegated.
 * 
 * @author Martin Zimmermann
 */
public class EventHelper {

    /**
     * List of event receivers
     */
    private final List<EventReceiverObject> receivers = new ArrayList<EventReceiverObject>();
    /**
     * Queue for new events
     */
    private final LinkedBlockingQueue queue = new LinkedBlockingQueue();
    /**
     * Producer of the events
     */
    private Object producer;

    /**
     * Constructor
     * @param producer Producer of the events
     */
    public EventHelper(Object producer) {
        this.producer = producer;
    }

    /**
     *Holder for event receiver objects. This objects are stored in the receivers list.
     */
    private class EventReceiverObject {

        private Object o;
        private Method m;
        private Class c;
        private boolean exact;

        public EventReceiverObject(Object o, Method m, boolean exact) {
            this.o = o;
            this.m = m;
            this.c = m.getParameterTypes()[0];
            this.exact = exact;
        }
    }

    /**
     * Register all methods with the EventReceiver annotation
     * @param o Object to process
     */
    public void registerReceiver(Object o) {
        for (Method m : o.getClass().getMethods()) {
            EventReceiver er = (EventReceiver) m.getAnnotation(EventReceiver.class);
            if (er != null) {
                for (String handleFor : er.handleFor()) {
                    boolean doAdd = true;
                    if (!handleFor.equals("")) {
                        try {
                            Field f = o.getClass().getDeclaredField(handleFor);
                            f.setAccessible(true);

                            if (f.get(o) != producer) {
                                doAdd = false;
                            }
                        } catch (Exception ex) {
                            Logger.getLogger(EventHelper.class.getName()).log(Level.WARNING, "No such field: " + handleFor, ex);
                            doAdd = false;
                        }
                    }
                    Class[] params = m.getParameterTypes();
                    if (params.length == 1 && doAdd) {
                        synchronized (receivers) {
                            receivers.add(new EventReceiverObject(o, m, er.exact()));
                        }
                    }
                }
            }
        }
    }

    /**
     * Unregister all receivers of the specified object
     * @param o Object to unregister
     */
    public void unregisterReceiver(Object o) {
        synchronized (receivers) {
            Iterator<EventReceiverObject> it = receivers.iterator();
            while (it.hasNext()) {
                if (it.next().o == o) {
                    it.remove();
                }
            }
        }
    }

    /**
     * Fire a new event. The event will be passed to the queue.
     * @param result
     */
    public synchronized void fireEvent(Object result) {
        try {
            int size = queue.size();
            queue.put(result);
            if (size == 0) {
                new Thread(sendEvents).start();
            }
        } catch (InterruptedException ex) {
            Logger.getLogger(EventHelper.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    /**
     * Runnable to send events in a separate thread
     */
    private Runnable sendEvents = new Runnable() {

        public void run() {
            while (!queue.isEmpty()) {
                try {
                    Object result = queue.take();
                    synchronized (receivers) {
                        for (EventReceiverObject er : receivers) {
                            boolean doSend = ((!er.exact && er.c.isInstance(result)) || (er.exact && er.c == result.getClass()));
                            if (doSend) {
                                try {
                                    er.m.invoke(er.o, result);
                                } catch (Exception ex) {
                                    Logger.getLogger(EventHelper.class.getName()).log(Level.SEVERE, null, ex);
                                }
                            }
                        }
                    }
                } catch (Exception ex) {
                    Logger.getLogger(EventHelper.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
    };
}
